Aprende a implementar la estimaci贸n de progreso y la predicci贸n del tiempo de finalizaci贸n usando el hook useFormStatus de React, mejorando la experiencia del usuario en aplicaciones con muchos datos.
Estimaci贸n de Progreso con useFormStatus de React: Predicci贸n del Tiempo de Finalizaci贸n
El hook useFormStatus de React, introducido en React 18, proporciona informaci贸n valiosa sobre el estado del env铆o de un formulario. Aunque no ofrece directamente una estimaci贸n del progreso, podemos aprovechar sus propiedades y otras t茅cnicas para proporcionar a los usuarios una retroalimentaci贸n significativa durante los env铆os de formularios que pueden ser prolongados. Este post explora m茅todos para estimar el progreso y predecir el tiempo de finalizaci贸n al usar useFormStatus, lo que resulta en una experiencia m谩s atractiva y f谩cil de usar.
Entendiendo useFormStatus
Antes de sumergirnos en la estimaci贸n del progreso, recapitulemos r谩pidamente el prop贸sito de useFormStatus. Este hook est谩 dise帽ado para ser utilizado dentro de un elemento <form> que utiliza la prop action. Devuelve un objeto que contiene las siguientes propiedades:
pending: Un booleano que indica si el formulario se est谩 enviando actualmente.data: Los datos que se enviaron con el formulario (si el env铆o fue exitoso).method: El m茅todo HTTP utilizado para el env铆o del formulario (p. ej., 'POST', 'GET').action: La funci贸n pasada a la propactiondel formulario.error: Un objeto de error si el env铆o fall贸.
Aunque useFormStatus nos dice si el formulario se est谩 enviando, no proporciona ninguna informaci贸n directa sobre el progreso del env铆o, especialmente si la funci贸n action implica operaciones complejas o largas.
El Desaf铆o de la Estimaci贸n de Progreso
El desaf铆o principal radica en que la ejecuci贸n de la funci贸n action es opaca para React. No sabemos inherentemente qu茅 tan avanzado est谩 el proceso. Esto es especialmente cierto para las operaciones del lado del servidor. Sin embargo, podemos emplear varias estrategias para superar esta limitaci贸n.
Estrategias para la Estimaci贸n de Progreso
Aqu铆 hay varios enfoques que puedes adoptar, cada uno con sus propias ventajas y desventajas:
1. Eventos Enviados por el Servidor (SSE) o WebSockets
La soluci贸n m谩s robusta suele ser enviar actualizaciones de progreso desde el servidor al cliente. Esto se puede lograr usando:
- Eventos Enviados por el Servidor (SSE): Un protocolo unidireccional (servidor a cliente) que permite al servidor enviar actualizaciones al cliente a trav茅s de una 煤nica conexi贸n HTTP. SSE es ideal cuando el cliente solo necesita *recibir* actualizaciones.
- WebSockets: Un protocolo de comunicaci贸n bidireccional que proporciona una conexi贸n persistente entre el cliente y el servidor. Los WebSockets son adecuados para actualizaciones en tiempo real en ambas direcciones.
Ejemplo (SSE):
Lado del servidor (Node.js):
const express = require('express');
const app = express();
app.get('/progress', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
let progress = 0;
const interval = setInterval(() => {
progress += 10;
if (progress > 100) {
progress = 100;
clearInterval(interval);
res.write(`data: {"progress": ${progress}, "completed": true}\n\n`);
res.end();
} else {
res.write(`data: {"progress": ${progress}, "completed": false}\n\n`);
}
}, 500); // Simula una actualizaci贸n de progreso cada 500ms
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
Lado del cliente (React):
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
useEffect(() => {
const eventSource = new EventSource('/progress');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
setProgress(data.progress);
if (data.completed) {
eventSource.close();
}
};
eventSource.onerror = (error) => {
console.error('EventSource failed:', error);
eventSource.close();
};
return () => {
eventSource.close();
};
}, []);
return (
<div>
<p>Progreso: {progress}%</p>
</div>
);
}
export default MyComponent;
Explicaci贸n:
- El servidor establece las cabeceras apropiadas para SSE.
- El servidor env铆a actualizaciones de progreso como eventos
data:. Cada evento es un objeto JSON que contiene elprogressy una banderacompleted. - El componente de React utiliza
EventSourcepara escuchar estos eventos. - El componente actualiza el estado (
progress) seg煤n los eventos recibidos.
Ventajas: Actualizaciones de progreso precisas, retroalimentaci贸n en tiempo real.
Desventajas: Requiere cambios en el lado del servidor, implementaci贸n m谩s compleja.
2. Sondeo (Polling) con un Endpoint de API
Si no puedes usar SSE o WebSockets, puedes implementar el sondeo (polling). El cliente env铆a solicitudes peri贸dicamente al servidor para verificar el estado de la operaci贸n.
Ejemplo:
Lado del servidor (Node.js):
const express = require('express');
const app = express();
// Simula una tarea de larga duraci贸n
let taskProgress = 0;
let taskId = null;
app.post('/start-task', (req, res) => {
taskProgress = 0;
taskId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); // Genera un ID de tarea 煤nico
// Simula el procesamiento en segundo plano
const interval = setInterval(() => {
taskProgress += 10;
if (taskProgress >= 100) {
taskProgress = 100;
clearInterval(interval);
}
}, 500);
res.json({ taskId });
});
app.get('/task-status/:taskId', (req, res) => {
if (req.params.taskId === taskId) {
res.json({ progress: taskProgress });
} else {
res.status(404).json({ message: 'Task not found' });
}
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
Lado del cliente (React):
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [taskId, setTaskId] = useState(null);
const startTask = async () => {
const response = await fetch('/start-task', { method: 'POST' });
const data = await response.json();
setTaskId(data.taskId);
};
useEffect(() => {
if (!taskId) return;
const interval = setInterval(async () => {
const response = await fetch(`/task-status/${taskId}`);
const data = await response.json();
setProgress(data.progress);
if (data.progress === 100) {
clearInterval(interval);
}
}, 1000); // Sondear cada 1 segundo
return () => clearInterval(interval);
}, [taskId]);
return (
<div>
<button onClick={startTask} disabled={taskId !== null}>Iniciar Tarea</button>
{taskId && <p>Progreso: {progress}%</p>}
</div>
);
}
export default MyComponent;
Explicaci贸n:
- El cliente inicia una tarea llamando a
/start-task, recibiendo untaskId. - Luego, el cliente sondea
/task-status/:taskIdperi贸dicamente para obtener el progreso.
Ventajas: Relativamente simple de implementar, no requiere conexiones persistentes.
Desventajas: Puede ser menos preciso que SSE/WebSockets, introduce latencia debido al intervalo de sondeo, carga el servidor debido a las solicitudes frecuentes.
3. Actualizaciones Optimistas y Heur铆sticas
En algunos casos, puedes usar actualizaciones optimistas combinadas con heur铆sticas para proporcionar una estimaci贸n razonable. Por ejemplo, si est谩s subiendo archivos, puedes rastrear el n煤mero de bytes subidos del lado del cliente y estimar el progreso bas谩ndote en el tama帽o total del archivo.
Ejemplo (Subida de Archivo):
import React, { useState } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [file, setFile] = useState(null);
const handleFileChange = (event) => {
setFile(event.target.files[0]);
};
const handleSubmit = async (event) => {
event.preventDefault();
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentage = Math.round((event.loaded * 100) / event.total);
setProgress(percentage);
}
});
xhr.open('POST', '/upload'); // Reemplaza con tu endpoint de subida
xhr.send(formData);
xhr.onload = () => {
if (xhr.status === 200) {
console.log('Upload complete!');
} else {
console.error('Upload failed:', xhr.status);
}
};
xhr.onerror = () => {
console.error('Upload failed');
};
} catch (error) {
console.error('Upload error:', error);
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input type="file" onChange={handleFileChange} />
<button type="submit" disabled={!file}>Subir</button>
</form>
<p>Progreso: {progress}%</p>
</div>
);
}
export default MyComponent;
Explicaci贸n:
- El componente utiliza un objeto
XMLHttpRequestpara subir el archivo. - El detector de eventos
progressenxhr.uploadse usa para rastrear el progreso de la subida. - Las propiedades
loadedytotaldel evento se utilizan para calcular el porcentaje completado.
Ventajas: Solo del lado del cliente, puede proporcionar retroalimentaci贸n inmediata.
Desventajas: La precisi贸n depende de la fiabilidad de la heur铆stica, puede no ser adecuado para todo tipo de operaciones.
4. Descomponer la Acci贸n en Pasos m谩s Peque帽os
Si la funci贸n action realiza m煤ltiples pasos distintos, puedes actualizar la UI despu茅s de cada paso para indicar el progreso. Esto requiere modificar la funci贸n action para que proporcione actualizaciones.
Ejemplo:
import React, { useState } from 'react';
async function myAction(setProgress) {
setProgress(10);
await someAsyncOperation1();
setProgress(40);
await someAsyncOperation2();
setProgress(70);
await someAsyncOperation3();
setProgress(100);
}
function MyComponent() {
const [progress, setProgress] = useState(0);
const handleSubmit = async () => {
await myAction(setProgress);
};
return (
<div>
<form onSubmit={handleSubmit}>
<button type="submit">Enviar</button>
</form>
<p>Progreso: {progress}%</p>
</div>
);
}
export default MyComponent;
Explicaci贸n:
- La funci贸n
myActionacepta una devoluci贸n de llamada (callback)setProgress. - Actualiza el estado del progreso en varios puntos durante su ejecuci贸n.
Ventajas: Control directo sobre las actualizaciones de progreso.
Desventajas: Requiere modificar la funci贸n action, puede ser m谩s complejo de implementar si los pasos no son f谩cilmente divisibles.
Predicci贸n del Tiempo de Finalizaci贸n
Una vez que tienes actualizaciones de progreso, puedes usarlas para predecir el tiempo restante estimado. Un enfoque simple es rastrear el tiempo que se tarda en alcanzar un cierto nivel de progreso y extrapolar para estimar el tiempo total.
Ejemplo (Simplificado):
import React, { useState, useEffect, useRef } from 'react';
function MyComponent() {
const [progress, setProgress] = useState(0);
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState(null);
const startTimeRef = useRef(null);
useEffect(() => {
if (progress > 0 && startTimeRef.current === null) {
startTimeRef.current = Date.now();
}
if (progress > 0) {
const elapsedTime = Date.now() - startTimeRef.current;
const estimatedTotalTime = (elapsedTime / progress) * 100;
const remainingTime = estimatedTotalTime - elapsedTime;
setEstimatedTimeRemaining(Math.max(0, remainingTime)); // Asegura que no sea negativo
}
}, [progress]);
// ... (resto del componente y actualizaciones de progreso como se describe en secciones anteriores)
return (
<div>
<p>Progreso: {progress}%</p>
{estimatedTimeRemaining !== null && (
<p>Tiempo Restante Estimado: {Math.round(estimatedTimeRemaining / 1000)} segundos</p>
)}
</div>
);
}
export default MyComponent;
Explicaci贸n:
- Almacenamos el tiempo de inicio cuando el progreso se actualiza por primera vez.
- Calculamos el tiempo transcurrido y lo usamos para estimar el tiempo total.
- Calculamos el tiempo restante restando el tiempo transcurrido del tiempo total estimado.
Consideraciones Importantes:
- Precisi贸n: Esta es una predicci贸n *muy* simplificada. Las condiciones de la red, la carga del servidor y otros factores pueden afectar significativamente la precisi贸n. T茅cnicas m谩s sofisticadas, como promediar sobre m煤ltiples intervalos, pueden mejorar la precisi贸n.
- Retroalimentaci贸n Visual: Indica claramente que el tiempo es una *estimaci贸n*. Mostrar rangos (p. ej., "Tiempo restante estimado: 5-10 segundos") puede ser m谩s realista.
- Casos L铆mite: Maneja los casos l铆mite donde el progreso es muy lento al principio. Evita dividir por cero o mostrar estimaciones excesivamente grandes.
Combinando useFormStatus con la Estimaci贸n de Progreso
Aunque useFormStatus en s铆 no proporciona informaci贸n de progreso, puedes usar su propiedad pending para habilitar o deshabilitar el indicador de progreso. Por ejemplo:
import React, { useState } from 'react';
import { useFormStatus } from 'react-dom';
// ... (L贸gica de estimaci贸n de progreso de los ejemplos anteriores)
function MyComponent() {
const [progress, setProgress] = useState(0);
const { pending } = useFormStatus();
const handleSubmit = async (formData) => {
// ... (Tu l贸gica de env铆o del formulario, incluyendo actualizaciones al progreso)
};
return (
<form action={handleSubmit}>
<button type="submit" disabled={pending}>Enviar</button>
{pending && <p>Progreso: {progress}%</p>}
</form>
);
}
En este ejemplo, el indicador de progreso solo se muestra mientras el formulario est谩 pendiente (es decir, mientras useFormStatus.pending es true).
Mejores Pr谩cticas y Consideraciones
- Prioriza la Precisi贸n: Elige una t茅cnica de estimaci贸n de progreso que sea apropiada para el tipo de operaci贸n que se est谩 realizando. SSE/WebSockets generalmente proporcionan los resultados m谩s precisos, mientras que las heur铆sticas pueden ser suficientes para tareas m谩s simples.
- Proporciona Retroalimentaci贸n Visual Clara: Usa barras de progreso, spinners u otras se帽ales visuales para indicar que una operaci贸n est谩 en curso. Etiqueta claramente el indicador de progreso y, si aplica, el tiempo restante estimado.
- Maneja los Errores con Elegancia: Si ocurre un error durante la operaci贸n, muestra un mensaje de error informativo al usuario. Evita dejar el indicador de progreso atascado en un cierto porcentaje.
- Optimiza el Rendimiento: Evita realizar operaciones computacionalmente costosas en el hilo de la UI, ya que esto puede afectar negativamente el rendimiento. Usa web workers u otras t茅cnicas para descargar trabajo a hilos en segundo plano.
- Accesibilidad: Aseg煤rate de que los indicadores de progreso sean accesibles para usuarios con discapacidades. Usa atributos ARIA para proporcionar informaci贸n sem谩ntica sobre el progreso de la operaci贸n. Por ejemplo, usa
aria-valuenow,aria-valueminyaria-valuemaxen una barra de progreso. - Localizaci贸n: Al mostrar el tiempo restante estimado, ten en cuenta los diferentes formatos de hora y las preferencias regionales. Usa una biblioteca como
date-fnsomoment.jspara formatear la hora apropiadamente para la configuraci贸n regional del usuario. - Internacionalizaci贸n: Los mensajes de error y otros textos deben ser internacionalizados para soportar m煤ltiples idiomas. Usa una biblioteca como
i18nextpara gestionar las traducciones.
Conclusi贸n
Aunque el hook useFormStatus de React no proporciona directamente capacidades de estimaci贸n de progreso, puedes combinarlo con otras t茅cnicas para ofrecer a los usuarios una retroalimentaci贸n significativa durante los env铆os de formularios. Usando SSE/WebSockets, sondeo, actualizaciones optimistas o descomponiendo las acciones en pasos m谩s peque帽os, puedes crear una experiencia m谩s atractiva y f谩cil de usar. Recuerda priorizar la precisi贸n, proporcionar una retroalimentaci贸n visual clara, manejar los errores con elegancia y optimizar el rendimiento para asegurar una experiencia positiva para todos los usuarios, sin importar su ubicaci贸n o contexto.